package com.helloscala.model

import org.joda.time.DateTime
import org.squeryl.KeyedEntity
import org.squeryl.annotations.Transient

import net.liftweb.util.StringHelpers
import net.liftweb.common.{Empty, Full, Failure, Box}

import yangbajing.util.Imports.Y
import yangbajing.util.{Pagination, PagerID}

import MyTypeMode._
import com.helloscala.common.SexCode
import com.helloscala.web.HelloURI

object MUser {
  def findNickById(id: String): Box[String] =
    transaction(from(Entities.users)(u => where(u.id === id) select u.nick).singleOption) match {
      case Some(Some(v)) => Full(v)
      case _ => Empty
    }

  def findAllIds(offset: Int, limit: Int): List[String] =
    transaction(from(Entities.users)(u => select(u.id)).page(offset, limit).toList)

  def findAllIds(): List[String] =
    transaction(from(Entities.users)(u => select(u.id)).toList)

  def changePassword(id: String, password: String): Box[_] =
    Y.tryBox {
      require(allowedPassword(password), "密码不符合要求")

      transaction {
        val po = from(Entities.userPasswords)(up => where(up.id === id) select up).singleOption
        require(po.isDefined, s"账号：$id 不存在")

        val up = po.get
        val newPassword = Y.ySha1(up.salt + password)

        update(Entities.userPasswords)(up =>
          where(up.id === id)
            set (up.password := newPassword))
      }
    }

  def haveId(id: String): Boolean =
    size(id = Option(id)) > 0L

  def haveEmail(email: String): Boolean =
    size(email = Option(email)) > 0L

  def pagination(_curPage: Int, _limit: Int
                  ) = new Pagination[MUser] {
    val curPage: Int = _curPage
    val limit: Int = _limit
    val total: Long = size()
    val page: List[MUser] =
      transaction(from(Entities.users)(u =>
        select(u) orderBy u.created_at.desc).page(offset, limit).toList)
  }

  def findAllPopular(limit: Int): List[MUser] = {
    findAll(0, limit)
  }

  def findAll(offset: Int, limit: Int): List[MUser] = {
    transaction(from(Entities.users)(u =>
      select(u) orderBy u.created_at.desc).page(offset, limit).toList)
  }

  def size(
            id: Option[String] = None,
            email: Option[String] = None,
            safeEmail: Option[String] = None,
            nick: Option[String] = None,
            sex: Option[SexCode.Value] = None): Long =
    transaction(from(Entities.users)(u =>
      where(u.id === id.? and
        (u.email === email.?) and
        (u.safe_email === safeEmail.?) and
        (u.nick === nick.?) and
        (u.sex === sex.?)
      ) compute (count(u.id))))

  def sizeOr(
              id: Option[String] = None,
              email: Option[String] = None,
              safeEmail: Option[String] = None,
              nick: Option[String] = None,
              sex: Option[SexCode.Value] = None): Long =
    transaction(from(Entities.users)(u =>
      where(u.id === id.? or
        (u.email === email.?) or
        (u.safe_email === safeEmail.?) or
        (u.nick === nick.?) or
        (u.sex === sex.?)
      ) compute (count(u.id))))

  def findById(id: String): Box[MUser] =
    transaction(from(Entities.users)(u =>
      where((if (allowedEmail(id)) u.email === id else u.id === id)
      ) select (u)).singleOption)

  def findById(id: String, password: String): Box[MUser] =
    Y.tryBox {
      require(allowedId(id) || allowedEmail(id), "账号不符合要求，请使用用户名或邮箱登录")
      require(allowedPassword(password), "密码无效，字符范围：[6-32]")

      transaction(from(Entities.users, Entities.userPasswords)((u, up) =>
        where(u.id === up.id and
          (if (allowedEmail(id)) u.email === id else u.id === id)
        ) select (u -> up)).singleOption) match {
        case Some((user, up)) =>
          require(up.matchPassword(password), "密码不正确")
          user
        case _ =>
          throw new RuntimeException("账号不存在")
      }
    }

  def save(user: MUser): Box[MUser] =
    Y.tryBox {
      require(allowedId(user.id), "账号不符合要求。只能使用英文小写字符、数字、'-'和'_'，且字符数为[2, 32]范围，首字符不能使用数字。")
      require(allowedEmail(user.email), "邮箱不符合要求")
      transaction(Entities.users update user)
      user
    }

  def insert(id: String, email: String, password: String, roles: List[String] = Nil): Box[MUser] =
    Y.tryBox {
      require(allowedId(id), s"账户：$id 无效")
      require(allowedEmail(email), s"邮箱地址：$email 无效")
      require(allowedPassword(password), s"密码：$password 无效")
      require(!haveId(id), s"账户：$id 已存在")
      require(!haveEmail(email), s"账户：$id 已存在")

      val (newPassword, salt) = realPassword(password)
      val userPassword = new MUserPassword(id, newPassword, salt)
      val user = MUser(id, email)

      transaction {
        val u = Entities.users insert user
        Entities.userPasswords insert userPassword
        if (roles.nonEmpty) Entities.usersRoles insert roles.map(v => new MUserRole(u.id, v))
        u
      }
    }

  /**
   *
   * @param password
   * @return (新的密码, salt值)
   */
  def realPassword(password: String): (String, String) = {
    val salt = StringHelpers.randomString(8)
    val newPassword = Y.ySha1(salt + password)

    (newPassword, salt)
  }

  def checkAccount(v: String): Box[Unit] =
    if (!allowedId(v)) Failure(s"账号：$v 无效")
    else if (size(id = Some(v)) > 0L) Failure(s"账号：$v 已存在")
    else Full(())

  def checkEmail(v: String): Box[Unit] =
    if (!allowedEmail(v)) Failure(s"安全邮箱：$v 无效")
    else Full(())

  def allowedEmail(email: String): Boolean =
    (email ne null) && Y.emailValidate(email)

  def allowedPassword(password: String): Boolean =
    (password ne null) && (password.length > 5)

  def allowedId(id: String): Boolean =
    !invalidId(id)

  def invalidId(id: String): Boolean = {
    (id eq null) ||
      id.length <= 2 ||
      id.length > 32 ||
      !_allowedLetterChar(id.charAt(0)) ||
      id.exists(c => !_allowedChar(c)) ||
      HelloURI.invalidSiteId.contains(id)
  }

  @inline
  private def _allowedLetterChar(c: Char): Boolean =
    c == '_' || c == '-' || (c >= 'a' && c <= 'z')

  @inline
  private def _allowedChar(c: Char): Boolean =
    _allowedLetterChar(c) || Character.isDigit(c)

}

case class MUser(
                  // 用于个人空间标识。只能使用英文字母、数字和下划线，且必须使用字母开头，大于等于3个字符，小于等于32个字符。
                  id: String,

                  email: String,

                  var nick: Option[String] = None,

                  var sex: Option[SexCode.Value] = None,

                  var safe_email: Option[String] = None,

                  var signature: Option[String] = None,

                  created_at: DateTime = DateTime.now()) extends KeyedEntity[String] with PagerID[String] {

  def this() = this("", "")

  def isAdmin: Boolean = roles.exists(r => r.id == "admin" || r.id == "root")

  def nickOrId = nick getOrElse id

  @Transient
  lazy val roles = MRole.findById(id)

  def documentCount: Long =
    MDocument.size(author = Some(id))

  def tagSize: Long =
    MTag.size(author = Some(id))

  def password: MUserPassword =
    transaction(from(Entities.userPasswords)(up => where(up.id === id) select (up)).single)
}
